using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using PloobsEngine.Light;
using PloobsEngine.Cameras;
using PloobsEngine.Material;
using PloobsEngine.Modelo;
using Microsoft.Xna.Framework.Input;

namespace PloobsEngine.SceneControl
{
    public enum DirectionalShadowFilteringType
    {
        PCF2x2 = 0,
        PCF3x3 = 1,
        PCF5x5 = 2,
        PCF7x7 = 3
    }

	public class ShadowRenderer
	{
        int shadowMapSize = 512;        
        const int NumSplits = 4;
        private float splitConstant = 0.95f;
        
		DepthStencilBuffer dsBuffer;
		DepthStencilBuffer oldDS;        
        RenderTarget2D shadowMap;
		Effect shadowMapEffect;
        RenderTarget2D shadowOcclusion;       
                

		Vector3[] frustumCornersVS = new Vector3[8];
        Vector3[] frustumCornersWS = new Vector3[8];
        Vector3[] frustumCornersLS = new Vector3[8];
        Vector3[] farFrustumCornersVS = new Vector3[4];
        Vector3[] splitFrustumCornersVS = new Vector3[8];                
        Matrix[] lightViewProjectionMatrices = new Matrix[NumSplits];
        Vector2[] lightClipPlanes = new Vector2[NumSplits];
        float[] splitDepths = new float[NumSplits + 1];
        private QuadRender quadRenderer;        
        bool showCascadeSplits = false;
        DirectionalShadowFilteringType filteringType = DirectionalShadowFilteringType.PCF3x3;
        EffectTechnique[] shadowOcclusionTechniques = new EffectTechnique[4];
        Viewport vp;

        /// <summary>
        /// Default 512
        /// </summary>
        public int ShadowMapSize
        {
            get { return shadowMapSize; }
            set { shadowMapSize = value; }
        }

        /// <summary>
        /// Default 0.95
        /// Tune the sizes of the splits (exponecial constant)
        /// </summary>
        public float SplitConstant
        {
            get { return splitConstant; }
            set { splitConstant = value; }
        }


        public DirectionalShadowFilteringType ShadowFilteringType
        {
            get { return filteringType; }
            set { filteringType = value; }
        }

        public bool ShowCascadeSplits
        {
            get { return showCascadeSplits; }
            set { showCascadeSplits = value; }
        }

        public ShadowRenderer() 
        {
        }

		/// <summary>
		/// Creates the renderer
		/// </summary>		
		public ShadowRenderer(int ShadowMapSize )
		{
            this.ShadowMapSize = ShadowMapSize;         
		}

        internal void Load()
        {
            // Load the effect we need
            shadowMapEffect = EngineStuff.InternalContentManager.GetAsset<Effect>("ShadowMap");

            // Create the shadow map, using a 32-bit floating-point surface format
            shadowMap = EngineStuff.GetDefaultBuffer(ShadowMapSize * NumSplits, ShadowMapSize, SurfaceFormat.Single);

            // Create a depth-stencil surface using the same dimensions as the shadow map
            dsBuffer = EngineStuff.GetDefaultDepthStencil(ShadowMapSize * NumSplits, ShadowMapSize, DepthFormat.Depth24Stencil8);

            // Create the shadow occlusion texture using the same dimensions as the backbuffer
            shadowOcclusion = EngineStuff.GetDefaultColorBuffer();

            quadRenderer = new QuadRender(EngineStuff.GraphicsDevice);

            // We'll keep an array of EffectTechniques that will let us map a
            // ShadowFilteringType to a technique for calculating shadow occlusion
            shadowOcclusionTechniques[0] = shadowMapEffect.Techniques["CreateShadowTerm2x2PCF"];
            shadowOcclusionTechniques[1] = shadowMapEffect.Techniques["CreateShadowTerm3x3PCF"];
            shadowOcclusionTechniques[2] = shadowMapEffect.Techniques["CreateShadowTerm5x5PCF"];
            shadowOcclusionTechniques[3] = shadowMapEffect.Techniques["CreateShadowTerm7x7PCF"];
        }

        

		/// <summary>
		/// Renders a list of models to the shadow map, and returns a surface 
		/// containing the shadow occlusion factor
		/// </summary>
		/// <param name="modelList">The list of models to render</param>
		/// <param name="depthTexture">Texture containing depth for the scene</param>
		/// <param name="light">The light for which the shadow is being calculated</param>
		/// <param name="mainCamera">The camera viewing the scene containing the light</param>
		/// <returns>The shadow occlusion texture</returns>
		internal RenderTarget2D Render(DirectionalLight light,
                                    ICamera mainCamera, IWorld world, IDeferredGBuffer deferredGBuffer)
									
		{
			    vp = EngineStuff.Viewport;
				// Set our targets
				oldDS = EngineStuff.GraphicsDevice.DepthStencilBuffer;
				EngineStuff.GraphicsDevice.DepthStencilBuffer = dsBuffer;
				EngineStuff.GraphicsDevice.SetRenderTarget(0, shadowMap);
				EngineStuff.GraphicsDevice.Clear(ClearOptions.Target, Color.White, 1.0f, 0);
				EngineStuff.GraphicsDevice.Clear(ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);                

				// Get corners of the main camera's bounding frustum
				Matrix cameraTransform, viewMatrix;                				                
                viewMatrix = mainCamera.View;
                cameraTransform = Matrix.Invert(viewMatrix);
				mainCamera.BoundingFrustum.GetCorners(frustumCornersWS);
				Vector3.Transform(frustumCornersWS, ref viewMatrix, frustumCornersVS);
				for (int i = 0; i < 4; i++)
					farFrustumCornersVS[i] = frustumCornersVS[i + 4];

                // Calculate the cascade splits.  We calculate these so that each successive
                // split is larger than the previous, giving the closest split the most amount
                // of shadow detail.  
                float N = NumSplits;
                float near = mainCamera.NearPlane, far = mainCamera.FarPlane;
                splitDepths[0] = near;
                splitDepths[NumSplits] = far;                
                for (int i = 1; i < splitDepths.Length - 1; i++)
                    splitDepths[i] = splitConstant * near * (float)Math.Pow(far / near, i / N) + (1.0f - splitConstant) * ((near + (i / N)) * (far - near));

                // Render our scene geometry to each split of the cascade
                for (int i = 0; i < NumSplits; i++)
                {
                    float minZ = splitDepths[i];
                    float maxZ = splitDepths[i + 1];

                    CalculateFrustum(light, mainCamera, minZ, maxZ,out lightViewProjectionMatrices[i]);                    

                    RenderShadowMap(i,world);
                }

                EngineStuff.GraphicsDevice.SetRenderTarget(0, null);
                DeferredDebugImageRender dir2 = new DeferredDebugImageRender(new Rectangle(0, 0, 250, 250), shadowMap.GetTexture());
                DeferredRenderTechnic.DebugImages.Add(dir2);

                EngineStuff.Viewport = vp;
				RenderShadowOcclusion(mainCamera,light,deferredGBuffer);
				return shadowOcclusion;						
			
		}        

		/// <summary>
		/// Determines the size of the frustum needed to cover the viewable area,
		/// then creates an appropriate orthographic projection.
		/// </summary>
		/// <param name="light">The directional light to use</param>
		/// <param name="mainCamera">The camera viewing the scene</param>
        protected void CalculateFrustum(DirectionalLight light, ICamera mainCamera, float minZ, float maxZ,out Matrix viewProjection)
		{
            // Shorten the view frustum according to the shadow view distance
            Matrix cameraMatrix = Matrix.Invert(mainCamera.View);            

            for (int i = 0; i < 4; i++)
                splitFrustumCornersVS[i] = frustumCornersVS[i + 4] * (minZ / mainCamera.FarPlane);

            for (int i = 4; i < 8; i++)
                splitFrustumCornersVS[i] = frustumCornersVS[i] * (maxZ / mainCamera.FarPlane);

            Vector3.Transform(splitFrustumCornersVS, ref cameraMatrix, frustumCornersWS);

			// Find the centroid
			Vector3 frustumCentroid = new Vector3(0,0,0);
			for (int i = 0; i < 8; i++)
				frustumCentroid += frustumCornersWS[i];
			frustumCentroid /= 8;

			// Position the shadow-caster camera so that it's looking at the centroid,
			// and backed up in the direction of the sunlight
            float distFromCentroid = MathHelper.Max((maxZ - minZ), Vector3.Distance(splitFrustumCornersVS[4], splitFrustumCornersVS[5])) + 50;
			Matrix view = Matrix.CreateLookAt(frustumCentroid - (light.LightDirection * distFromCentroid), frustumCentroid, new Vector3(0,1,0));

			// Determine the position of the frustum corners in light space
            Vector3.Transform(frustumCornersWS, ref view, frustumCornersLS);

			// Calculate an orthographic projection by sizing a bounding box 
			// to the frustum coordinates in light space
			Vector3 mins = frustumCornersLS[0];
			Vector3 maxes = frustumCornersLS[0];
			for (int i = 0; i < 8; i++)
			{
				if (frustumCornersLS[i].X > maxes.X)
					maxes.X = frustumCornersLS[i].X;
				else if (frustumCornersLS[i].X < mins.X)
					mins.X = frustumCornersLS[i].X;
				if (frustumCornersLS[i].Y > maxes.Y)
					maxes.Y = frustumCornersLS[i].Y;
				else if (frustumCornersLS[i].Y < mins.Y)
					mins.Y = frustumCornersLS[i].Y;
				if (frustumCornersLS[i].Z > maxes.Z)
					maxes.Z = frustumCornersLS[i].Z;
				else if (frustumCornersLS[i].Z < mins.Z)
					mins.Z = frustumCornersLS[i].Z;
			}     

            // Create an orthographic camera for use as a shadow caster            
            viewProjection = view * Matrix.CreateOrthographicOffCenter(mins.X, maxes.X, mins.Y, maxes.Y, -maxes.Z - light.NearClipOffset, -mins.Z);
            
		}       

		/// <summary>
		/// Renders the shadow map using the orthographic camera created in
		/// CalculateFrustum.
		/// </summary>
		/// <param name="modelList">The list of models to be rendered</param>        
        protected void RenderShadowMap(int splitIndex, IWorld world)
		{
            // Set the viewport for the current split            
            Viewport splitViewport = new Viewport();
            splitViewport.MinDepth = 0;
            splitViewport.MaxDepth = 1;
            splitViewport.Width = ShadowMapSize;
            splitViewport.Height = ShadowMapSize;
            splitViewport.X = splitIndex * ShadowMapSize;
            splitViewport.Y = 0;
            EngineStuff.GraphicsDevice.Viewport = splitViewport;

            // Set up the effect
            shadowMapEffect.CurrentTechnique = shadowMapEffect.Techniques["GenerateShadowMap"];            
            shadowMapEffect.Parameters["g_matViewProj"].SetValue(lightViewProjectionMatrices[splitIndex]);
                        
            // Draw the models
            DrawScene(EngineStuff.GetCurrentGameTime(), world);
		}

		/// <summary>
		/// Renders a texture containing the final shadow occlusion
		/// </summary>
        protected void RenderShadowOcclusion(ICamera mainCamera,DirectionalLight dl,  IDeferredGBuffer deferredGBuffer)
		{
			// Set the device to render to our shadow occlusion texture, and to use
			// the original DepthStencilSurface
			EngineStuff.GraphicsDevice.SetRenderTarget(0, shadowOcclusion);
			EngineStuff.GraphicsDevice.DepthStencilBuffer = oldDS;

            Matrix cameraTransform = Matrix.Invert(mainCamera.View);

            // We'll use these clip planes to determine which split a pixel belongs to
            for (int i = 0; i < NumSplits; i++)
            {
                lightClipPlanes[i].X = -splitDepths[i];
                lightClipPlanes[i].Y = -splitDepths[i + 1];
                                
                //lightCameras[i].GetViewProjMatrix(out lightViewProjectionMatrices[i]);
            }                         

			// Setup the Effect
			shadowMapEffect.CurrentTechnique = shadowOcclusionTechniques[(int)filteringType];
			shadowMapEffect.Parameters["g_matInvView"].SetValue(cameraTransform);
            shadowMapEffect.Parameters["g_matLightViewProj"].SetValue(lightViewProjectionMatrices);
			shadowMapEffect.Parameters["g_vFrustumCornersVS"].SetValue(farFrustumCornersVS);
            shadowMapEffect.Parameters["g_vClipPlanes"].SetValue(lightClipPlanes);
			shadowMapEffect.Parameters["ShadowMap"].SetValue(shadowMap.GetTexture());
			shadowMapEffect.Parameters["DepthTexture"].SetValue(deferredGBuffer[GBufferTypes.DEPH]);
            shadowMapEffect.Parameters["g_matInvProj"].SetValue(Matrix.Invert(mainCamera.Projection));
			shadowMapEffect.Parameters["g_vOcclusionTextureSize"].SetValue(new Vector2(shadowOcclusion.Width, shadowOcclusion.Height));
			shadowMapEffect.Parameters["g_vShadowMapSize"].SetValue(new Vector2(shadowMap.Width, shadowMap.Height));
            shadowMapEffect.Parameters["g_bShowSplitColors"].SetValue(showCascadeSplits);
            shadowMapEffect.Parameters["BIAS"].SetValue(dl.SHADOWBIAS);
            

			// Begin effect
			shadowMapEffect.Begin(SaveStateMode.None);
			shadowMapEffect.CurrentTechnique.Passes[0].Begin();

			// Draw the full screen quad		
            quadRenderer.Render(Vector2.One * -1, Vector2.One);

			// End the effect
			shadowMapEffect.CurrentTechnique.Passes[0].End();
			shadowMapEffect.End();



            EngineStuff.GraphicsDevice.SetRenderTarget(0, null);
            DeferredDebugImageRender dir2 = new DeferredDebugImageRender(new Rectangle(250, 0, 250, 250), shadowOcclusion.GetTexture());
            DeferredRenderTechnic.DebugImages.Add(dir2);

		}

        protected void DrawScene(GameTime gameTime, IWorld world)
        {
            foreach (IObject item in world.Objects)
            {
                if (item.Material.MaterialType == MaterialType.DEFERRED)
                {
                    if (item.Material.AffectedByShadow == true && item.Modelo != null)
                    {
                        if (item.Material.Shadder.DepthExtractor != null)
                        {
                            item.Material.Shadder.DepthExtractor.ExtractDepthFromObject(item, world.ActiveCamera.View, world.ActiveCamera.Projection);
                        }
                        else
                        {
                            IModelo modelo = item.Modelo;
                            Matrix[] boneTransforms = modelo.getBonesTransformation();
                            Matrix wld = item.getWorldMatrix();

                            for (int i = 0; i < modelo.MeshNumber; i++)
                            {
                                BatchInformation[] bi = modelo.GetBatchInformation(i);                                
                                Matrix w1 = Matrix.Multiply(wld, boneTransforms[modelo.GetParentBoneIndex(i)]);

                                for (int j = 0; j < bi.Length; j++)
                                {
                                    EngineStuff.GraphicsDevice.VertexDeclaration = bi[j].VertexDeclaration;
                                    EngineStuff.GraphicsDevice.Vertices[0].SetSource(modelo.GetVertexBuffer(i), bi[j].StreamOffset, bi[j].VertexStride);
                                    EngineStuff.GraphicsDevice.Indices = modelo.GetIndexBuffer(i);
                                    shadowMapEffect.Parameters["g_matWorld"].SetValue(w1);

                                    shadowMapEffect.Begin();
                                    shadowMapEffect.CurrentTechnique.Passes[0].Begin();
                                    EngineStuff.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, bi[j].BaseVertex, 0, bi[j].NumVertices, bi[j].StartIndex, bi[j].PrimitiveCount);
                                    shadowMapEffect.CurrentTechnique.Passes[0].End();
                                    shadowMapEffect.End();

                                }
                            }
                        }
                    }
                }
            }
        }

	}

}
